#!/usr/bin/env python3

# Copyright (c) 2005-2014 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import glob

from ._po import Flags
from ._po import BootMode
from ._po import RescueOsSpec
from ._config import ConfigBase
from ._plugin import PluginManager
from ._plugin import ContextListDistfiles
from ._kernel import KernelInstaller
from ._kernel import KernelUpdater
from ._exception import RunningEnvironmentError

from ._util import Util
from ._util import TempChdir
from ._boot_entry import BootEntryUtils
from ._bootloader import BootLoader
from ._check import Checker


class Bbki:

    def __init__(self, cfg):
        assert isinstance(cfg, ConfigBase)
        cfg._assertValid()
        self._cfg = cfg

        self._fsLayout = self._cfg.get_fs_layout()      # FIXME

        self._kernelInstaller = None
        self._kernelUpdaterDict = {}

        if Flags.BOOTLOADER in self._cfg.flags:
            assert len(self._cfg.get_mount_points()) > 0 and all([x.device is not None for x in self._cfg.get_mount_points()])
            self._bootloader = BootLoader(self, self._cfg.get_mount_points()[0], Util.findInList(self._cfg.get_mount_points(), key=lambda x: x.mountpoint == self._fsLayout.get_boot_dir()))
        else:
            if os.path.exists(self._fsLayout.get_boot_grub_dir()):
                raise RunningEnvironmentError("bootloader exists when it should not be")
            if os.path.exists(self._fsLayout.get_boot_efi_dir()):
                raise RunningEnvironmentError("bootloader exists when it should not be")

    def dispose(self):
        for x in list(self._kernelUpdaterDict.values()):
            x.dispose()
        assert len(self._kernelUpdaterDict) == 0

        if self._kernelInstaller is not None:
            self._kernelInstaller.dispose()
        assert self._kernelInstaller is None

    @property
    def rescue_os_spec(self):
        return RescueOsSpec(self)

    def get_pending_boot_entry(self):
        if Flags.BOOTLOADER in self._cfg.flags:
            if self._bootloader.getStatus() == BootLoader.STATUS_NORMAL:
                mbe = self._bootloader.getMainBootEntry()
                if mbe.has_kernel_files() and mbe.has_initramfs_files():
                    return mbe
        return None

    def get_newest_boot_entry(self):
        ret = self.get_boot_entries()
        if len(ret) > 0:
            return ret[0]
        else:
            return None

    def get_boot_entries(self):
        ret = []
        for kernelFile in sorted(os.listdir(self._fsLayout.get_boot_dir()), reverse=True):
            if kernelFile.startswith("kernel-"):
                ret.append(BootEntryUtils(self).new_from_postfix(kernelFile[len("kernel-"):]))
        return ret

    def get_history_boot_entries(self):
        if not os.path.exists(self._fsLayout.get_boot_history_dir()):
            return []

        ret = []
        for kernelFile in sorted(os.listdir(self._fsLayout.get_boot_history_dir()), reverse=True):
            if kernelFile.startswith("kernel-"):
                be = BootEntryUtils(self).new_from_postfix(kernelFile[len("kernel-"):], history_entry=True)
                if be.has_kernel_files() and be.has_initramfs_files():
                    ret.append(be)
        return ret

    def get_stable_flag(self):
        if Flags.BOOTLOADER in self._cfg.flags:
            if self._bootloader.getStatus() == BootLoader.STATUS_NORMAL:
                if self._bootloader.getStableFlag():
                    return True
        return False

    def get_kernel_installer(self, boot_entry_spec):
        assert self._kernelInstaller is None
        return KernelInstaller(self, boot_entry_spec)

    def get_kernel_updater(self, boot_entry):
        assert boot_entry not in self._kernelUpdaterDict
        return KernelUpdater(self, boot_entry)

    def install_bootloader(self, main_boot_entry, aux_os_list, aux_kernel_init_cmdline):
        assert Flags.BOOTLOADER in self._cfg.flags
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0
        self._bootloader.install(self._cfg.get_boot_mode(), main_boot_entry, aux_os_list, aux_kernel_init_cmdline)

    def update_bootloader(self, main_boot_entry=None, aux_os_list=None, aux_kernel_init_cmdline=None):
        assert Flags.BOOTLOADER in self._cfg.flags
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0
        assert self._bootloader.getStatus() == BootLoader.STATUS_NORMAL
        self._bootloader.update(main_boot_entry, aux_os_list, aux_kernel_init_cmdline)

    def set_stable_flag(self, value):
        assert Flags.BOOTLOADER in self._cfg.flags
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        # we use grub environment variable to store stable status
        if self._bootloader.getStatus() == BootLoader.STATUS_NORMAL:
            self._bootloader.setStableFlag(value)
        if value:
            raise RunningEnvironmentError("bootloader is not properly installed")

    def clean_boot_entry_files(self, pretend=False):
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        beList = self.get_boot_entries()

        # get to-be-deleted files in /boot
        bootFileList = None
        if True:
            tset = set(glob.glob(os.path.join(self._fsLayout.get_boot_dir(), "*")))                     # mark /boot/* (no recursion) as to-be-deleted
            if Flags.BOOTLOADER in self._cfg.flags:
                if self._bootloader.getStatus() == BootLoader.STATUS_NORMAL:
                    tset -= set(Util.globDirRecursively(self._fsLayout.get_boot_grub_dir()))            # don't delete boot-loader files
                    if self._cfg.get_boot_mode() == BootMode.EFI:
                        tset -= set(Util.globDirRecursively(self._fsLayout.get_boot_efi_dir()))
                    elif self._cfg.get_boot_mode() == BootMode.BIOS:
                        pass
                    else:
                        assert False
            if True:
                tset.discard(self._fsLayout.get_boot_rescue_os_dir())                                   # don't delete /boot/rescue
            for be in beList:
                tset -= set([be.spec_filepath])                                                         # don't delete files of pending-boot-entries
                if True:
                    assert Flags.KERNEL in self._cfg.flags
                    tset -= set([be.kernel_filepath, be.kernel_config_filepath])
                if Flags.INITRAMFS in self._cfg.flags:
                    tset -= set([be.initramfs_filepath, be.initramfs_tar_filepath])
            bootFileList = sorted(list(tset))

        # get to-be-deleted files in /lib/modules
        modulesFileList = BootEntryUtils(self).getRedundantKernelModulesDirs(beList)
        if modulesFileList == os.listdir(self._fsLayout.get_kernel_modules_dir()):
            modulesFileList.append(self._fsLayout.get_kernel_modules_dir())

        # get to-be-deleted files in /lib/firmware
        firmwareFileList = BootEntryUtils(self).getRedundantFirmwareFiles(beList)

        # delete files
        if not pretend:
            for fullfn in bootFileList:
                Util.forceDelete(fullfn)
            for fullfn in modulesFileList:
                Util.forceDelete(fullfn)
            for fullfn in firmwareFileList:
                Util.forceDelete(fullfn)

        # return value
        return (bootFileList, modulesFileList, firmwareFileList)

    def clean_distfiles(self, pretend=False):
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        # keep files
        keepFiles = []
        for be in self.get_boot_entries() + self.get_history_boot_entries():
            p = PluginManager.get_kernel_plugin(be.spec)
            context = ContextListDistfiles(self, p, be.get_kernel_version())
            with TempChdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
                keepFiles += self._p.list_distfiles(context)

            for addonName, p in PluginManager.get_kernel_addon_plugins(be.spec).items():
                context = ContextListDistfiles(self, p, be.get_kernel_addon_version(addonName))
                with TempChdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
                    keepFiles += self._p.list_distfiles(context)

        # get to be deleted files
        ret = []
        for fn in os.listdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
            if fn in ["git-src", "svn-src"]:
                bFound = False
                for fn2 in os.listdir(os.path.join(self._bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn)):
                    fn2 = os.path.join(fn, fn2)
                    if fn2 not in keepFiles:
                        ret.append(os.path.join(self._bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn2))
                    else:
                        bFound = True
                if not bFound and fn not in keepFiles:
                    ret.append(os.path.join(self._bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn))
                continue
            if fn not in keepFiles:
                ret.append(os.path.join(self._bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn))
                continue

        # delete files
        if not pretend:
            for fullfn in ret:
                Util.forceDelete(fullfn)

        # return value
        return ret

    def remove_all(self):
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        if Flags.BOOTLOADER in self._cfg.flags:
            self._bootloader.remove(bForce=True)
        Util.truncateDir(self._fsLayout.get_boot_dir())                # remove /boot/*
        Util.forceDelete(self._fsLayout.get_firmware_dir())            # remove /lib/firmware
        Util.forceDelete(self._fsLayout.get_kernel_modules_dir())      # remove /lib/modules

    def check_config(self, autofix=False, error_callback=None):
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        if error_callback is None:
            error_callback = self._doNothing

    def check_boot_entry_files(self, autofix=False, error_callback=None):
        assert self._kernelInstaller is None and len(self._kernelUpdaterDict) == 0

        obj = Checker(self, autofix, error_callback)
        obj.checkBootDir()
        obj.checkKernelModulesDir()
        obj.checkFirmwareDir()

    def _doNothing(self, msg):
        pass

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.dispose()
